package com.zmc.search.service;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zmc.common.annotation.EsQuery;
import com.zmc.common.annotation.EsUpdate;
import com.zmc.common.domain.es.EsBaseEntity;
import com.zmc.common.domain.es.EsPage;
import com.zmc.common.enums.es.EsQueryEnum;
import com.zmc.common.enums.es.EsSortEnum;
import com.zmc.common.utils.ClazzUtils;
import com.zmc.common.utils.StringUtils;
import com.zmc.search.exception.EsRequestException;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.index.reindex.UpdateByQueryRequest;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Stream;


/**
 * @Author：zmc
 * @Date：2024/11/29 9:51
 */


@Component
public class ElasticsearchService {

    public static final Logger log = LoggerFactory.getLogger(ElasticsearchService.class);

    @Autowired
    private RestHighLevelClient restHighLevelClient;


    /**
     * 添加索引
     */
    public boolean addIndex(String indexName) {
        if (StringUtils.isEmpty(indexName)) {
            log.error("添加索引时， 索引不能为空， indexName:{}", indexName);
            throw new EsRequestException("索引不能为空");
        }
        CreateIndexResponse createIndexResponse = null;
        try {
            //1.使用client获取操作索引对象
            IndicesClient indices = restHighLevelClient.indices();
            //2.具体操作获取返回值
            //2.1 设置索引名称
            CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
            createIndexResponse = indices.create(createIndexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("添加索引异常,indexName:{}", indexName, e);
            throw new EsRequestException("添加索引异常");
        }
        //3.根据返回值判断结果
        return createIndexResponse.isAcknowledged();
    }

    /**
     * 删除索引
     *
     * @param indexName
     */
    public boolean deleteIndex(String indexName) {
        if (StringUtils.isEmpty(indexName)) {
            log.error("添加索引时， 索引不能为空， indexName:{}", indexName);
            throw new EsRequestException("索引不能为空");
        }
        AcknowledgedResponse deleteRespone = null;
        try {
            //1.使用client获取操作索引对象
            IndicesClient indices = restHighLevelClient.indices();
            //2.具体操作获取返回值
            //2.1 设置索引名称
            //指定要删除的索引名称
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            deleteRespone = indices.delete(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("删除索引异常,indexName:{}", indexName, e);
            throw new EsRequestException("删除索引异常");

        }
        //3.根据返回值判断结果
        return deleteRespone.isAcknowledged();
    }

    /**
     * 创建数据
     *
     * @param indexName
     * @param id
     * @param data
     */
    public boolean addData(String indexName, String id, Object data) {
        if (StringUtils.isEmpty(indexName) || Objects.isNull(data)) {
            log.error("方法：addOrUpdateData， 索引不能为空， indexName:{}，data:{}", indexName, data);
            throw new EsRequestException("索引或者data不能为空");
        }
        try {
            BulkRequest bulkRequest = new BulkRequest();
            //准备文档
            String jsonString = JSONObject.toJSONString(data);
            //创建请求
            IndexRequest indexRequest = new IndexRequest(indexName).id(id);
            //指定文档内容
            indexRequest.source(jsonString, XContentType.JSON);
            //true 当存在相同的_id时，插入会出现异常； false 当存在相同_id时，插入会进行覆盖；
            indexRequest.create(true);
            bulkRequest.add(indexRequest);
            //通过client进行http请求
            BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
            return !bulk.hasFailures();
            //通过client进行http请求
        } catch (Exception e) {
            log.error("方法：addData，indexName{},id:{},data:{}", indexName, id, data, e);
            throw new EsRequestException("es创建数据异常");
        }
    }

    /**
     * 创建文档id存在则更新文档
     *
     * @param indexName
     * @param id
     * @param data
     * @throws IOException
     */
    public boolean addOrUpdateData(String indexName, String id, Object data) {
        if (StringUtils.isEmpty(indexName) || Objects.isNull(data)) {
            log.error("方法：addOrUpdateData， 索引不能为空， indexName:{}，data:{}", indexName, data);
            throw new EsRequestException("索引或者data不能为空");
        }
        try {
            BulkRequest bulkRequest = new BulkRequest();
            //准备文档
            String jsonString = JSONObject.toJSONString(data);
            //创建请求
            IndexRequest indexRequest = new IndexRequest(indexName).id(id);
            //指定文档内容
            indexRequest.source(jsonString, XContentType.JSON);
            //true 当存在相同的_id时，插入会出现异常； false 当存在相同_id时，插入会进行覆盖；
            indexRequest.create(true);
            bulkRequest.add(indexRequest);
            //通过client进行http请求
            BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
            return !bulk.hasFailures();
        } catch (Exception e) {
            log.error("elasticsearch addOrUpdateDoc error , meassage = {}", e.getMessage());
            //打印轨迹
            throw new EsRequestException("es创建数据异常");
        }
    }

    /**
     * 单条更新
     *
     * @param indexName
     * @param id
     * @param data
     * @return
     * @throws IOException
     */
    public boolean updateData(String indexName, String id, Object data) throws IOException {
        if (StringUtils.isEmpty(indexName) || StringUtils.isEmpty(id) || Objects.isNull(data)) {
            log.error("方法：addOrUpdateData， 索引、id、data不能为空， indexName:{}，id:{},data:{}", indexName, id, data);
            throw new EsRequestException("es updateData异常");
        }
        UpdateRequest updateRequest = new UpdateRequest(indexName, id);
        //准备文档
        String jsonString = JSONObject.toJSONString(data);
        Map jsonMap = JSONObject.parseObject(jsonString, Map.class);
        updateRequest.doc(jsonMap);
        updateRequest.timeout(TimeValue.timeValueSeconds(1));
        updateRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL);
        //数据为存储而不是更新
        UpdateResponse update = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
        return update.getGetResult().equals(DocWriteResponse.Result.UPDATED);
    }


    /**
     * 批量新增数据
     *
     * @param index
     * @param list
     * @return
     */
    public boolean addBatchData(String index, List<? extends EsBaseEntity> list) {
        if (list.size() > 100000) {
            log.error("es add batch data too large{}", list.size());
            throw new EsRequestException("es addBatchData异常");
        }
        BulkResponse bulk = null;
        try {
            BulkRequest request = new BulkRequest();
            list.forEach(data -> {
                String source = JSON.toJSONString(data);
                request.add(new IndexRequest(index).id(data.documentId).source(source, XContentType.JSON));
            });
            bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
            if (bulk.hasFailures()) {
                throw new EsRequestException("es 批量上架异常");
            }
            return true;
        } catch (Exception e) {
            log.error("elasticsearch addBatchData error , meassage = {}", e.getMessage());
            throw new EsRequestException("elasticsearch addBatchData error , meassage=" + e.getMessage());
        }
    }

    /**
     * 通过id删除数据
     *
     * @param indexName
     * @param id
     * @return
     */
    public boolean deleteDataById(String indexName, String id) {
        DeleteRequest deleteRequest = new DeleteRequest(indexName, id);
        DeleteResponse response = null;
        try {
            response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("elasticsearch deleteDocById error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
            throw new EsRequestException("elasticsearch deleteDataById error , meassage=" + e.getMessage());
        }
        return response.getResult().equals(DocWriteResponse.Result.DELETED);
    }


    /**
     * 通过条件删除数据
     *
     * @param indexName
     * @param object
     * @return
     */
    public boolean deleteDataByCondition(String indexName, Object object) {
        BulkByScrollResponse resp = null;
        try {
            DeleteByQueryRequest request = new DeleteByQueryRequest(indexName);
            // 更新时版本冲突
            request.setConflicts("proceed");
            //构建条件
            setDeleteCondition(object, request);
            // 刷新索引
            request.setRefresh(true);
            resp = restHighLevelClient.deleteByQuery(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("elasticsearch deleteDataByCondition error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
            throw new EsRequestException("elasticsearch deleteDataByCondition error , meassage=" + e.getMessage());
        }
        return resp.getStatus().getDeleted() > 0;
    }


    /**
     * 通过条件更新数据
     *
     * @param indexName: 索引
     * @param data：查询条件
     * @return boolean
     */
    public <T> boolean updateDataByCondition(String indexName, Object data , Class<T> c) {
        BulkByScrollResponse resp = null;
        try {
            UpdateByQueryRequest request = new UpdateByQueryRequest(indexName);
            //设置分片并行
            request.setSlices(2);
            //设置版本冲突时继续执行
            request.setConflicts("proceed");
            //构建条件
            setUpdateCondition(data, request);
            //设置更新完成后刷新索引 ps很重要如果不加可能数据不会实时刷新
            request.setRefresh(true);
            StringBuilder scriptContext = buildScriptContext(data, c);
            if (scriptContext.toString().isEmpty()) {
                throw new EsRequestException("elasticsearch updateDataByCondition error , 更新字段没有设置，无法进行更新");
            }
            //设置要修改的内容可以多个值多个用；隔开
            request.setScript(new Script(scriptContext.toString()));
            resp = restHighLevelClient.updateByQuery(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("elasticsearch updateDataByCondition error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
            throw new EsRequestException("elasticsearch updateDataByCondition error , meassage=" + e.getMessage());
        }
        return resp.getStatus().getUpdated() > 0;
    }

    /**
     * 根据id查询文档
     */
    public <T> T selectDataById(String indexName, String id, Class<T> c) {
        GetResponse response = null;
        try {
            //设置查询的索引、文档
            GetRequest indexRequest = new GetRequest(indexName, id);
            response = restHighLevelClient.get(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("elasticsearch selectDataById error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
        }
        String res = response.getSourceAsString();
        return JSONObject.parseObject(res, c);
    }


    /**
     * 条件查询
     *
     * @param indexName
     * @param obj       条件
     * @param c         返回对象类型
     * @return
     */
    public <T> List<T> selectDataList(String indexName, Object obj, Class<T> c) {
        List<T> res = null;
        try {
            // 创建检索请求
            SearchRequest searchRequest = new SearchRequest();
            // 指定索引
            searchRequest.indices(indexName);
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            //构建DSL
            buildSearchSourceBuilder(obj, searchSourceBuilder);
            searchRequest.source(searchSourceBuilder);
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //分析结果
            SearchHit[] hits = searchResponse.getHits().getHits();
            res = new ArrayList<>();
            for (SearchHit hit : hits
            ) {
                String data = hit.getSourceAsString();
                T t = JSONObject.parseObject(data, c);
                log.info("data={}", data);
                res.add(t);
            }
        } catch (Exception e) {
            log.error("elasticsearch selectDataList error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
            throw new EsRequestException("elasticsearch selectDataList error , meassage=" + e.getMessage());
        }
        return res;
    }

    /**
     * 条件查询
     *
     * @param indexName
     * @param obj       条件查询
     * @param c         返回对象类型
     * @return
     */
    public <T> EsPage<T> selectDataPage(String indexName, Integer pageNum, Integer pageSize, Object obj, Class<T> c) {
        List<T> res = null;
        //总记录数
        Integer total = 0;
        try {
            // 创建检索请求
            SearchRequest searchRequest = new SearchRequest();
            // 指定索引
            searchRequest.indices(indexName);
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            //构建DSL
            buildSearchSourceBuilder(obj, searchSourceBuilder);
            //设置分页
            searchSourceBuilder.from((pageNum - 1) * pageSize).size(pageSize);
            searchRequest.source(searchSourceBuilder);
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //分析结果
            SearchHit[] hits = searchResponse.getHits().getHits();
            total = new Long(searchResponse.getHits().getTotalHits().value).intValue();
            res = new ArrayList<>();
            for (SearchHit hit : hits) {
                String data = hit.getSourceAsString();
                T t = JSONObject.parseObject(data, c);
                log.info("data={}", data);
                res.add(t);
            }
        } catch (Exception e) {
            log.error("elasticsearch selectDataPage error , meassage = {}", e.getMessage());
            //打印轨迹
            log.error(e.getMessage(), e);
            throw new EsRequestException("elasticsearch selectDataList error , meassage=" + e.getMessage());
        }
        return new EsPage<>(pageNum, pageSize, total, res);
    }

    /**
     * 构建搜索条件(DSL)
     *
     * @param obj
     * @return
     */
    public void buildSearchSourceBuilder(Object obj, SearchSourceBuilder searchSourceBuilder) {
        searchSourceBuilder.query(getBoolQueryBuilder(obj, searchSourceBuilder));
    }


    /**
     * 构建删除条件(DSL)
     *
     * @param object
     * @param request
     */
    private void setDeleteCondition(Object object, DeleteByQueryRequest request) {
        request.setQuery(getBoolQueryBuilder(object, null));
    }

    /**
     * 构建修改条件DSL
     *
     * @param object
     * @param request
     */
    private void setUpdateCondition(Object object, UpdateByQueryRequest request) {
        request.setQuery(getBoolQueryBuilder(object, null));
    }


    /**
     * 构建DSL语句(通用)
     * 注: 若要新增查询方式，可在枚举中添加类型，然后下面写追加逻辑即可
     */
    private BoolQueryBuilder getBoolQueryBuilder(Object obj, SearchSourceBuilder searchSourceBuilder) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        Field[] fields = obj.getClass().getFields();
        for (Field field : fields) {
            //访问私有属性时必须设置，否则访问不到
            field.setAccessible(true);
            if (field.isAnnotationPresent(EsQuery.class)) {
                EsQuery annotation = field.getAnnotation(EsQuery.class);
                if (!annotation.open()) {
                    break;
                }
                String name = field.getName();
                Object value = ClazzUtils.getFieldValueByName(name, obj);
                String fieldName = annotation.name();
                //若注解设置name属性，则使用注解的name属性，否则使用自定义属性
                fieldName = StringUtils.isEmpty(fieldName) ? name : fieldName;
                //排序规则
                EsSortEnum esSortEnum = annotation.sort();
                sortRule(esSortEnum, fieldName, searchSourceBuilder);
                EsQueryEnum esQueryEnum = annotation.value();
                if (!Objects.isNull(value)) {
                    //查询规则
                    queryRule(esQueryEnum, fieldName, value, boolQuery);
                    //高亮处理
                    if (annotation.highlight() && searchSourceBuilder != null) {
                        HighlightBuilder highlightBuilder = new HighlightBuilder();
                        highlightBuilder.field(fieldName);
                        highlightBuilder.preTags("<b style='color:red'>");
                        highlightBuilder.postTags("</b>");
                        searchSourceBuilder.highlighter(highlightBuilder);
                    }
                }
            }
        }
        return boolQuery;
    }


    //查询规则
    private void sortRule(EsSortEnum esSortEnum, String fieldName, SearchSourceBuilder searchSourceBuilder) {
        switch (esSortEnum) {
            case ORDER_ASC:
                //升序
                if (searchSourceBuilder != null) {
                    searchSourceBuilder.sort(fieldName, SortOrder.ASC);
                }
                break;
            case ORDER_ESC:
                //降序
                if (searchSourceBuilder != null) {
                    searchSourceBuilder.sort(fieldName, SortOrder.DESC);
                }
                break;
            default:
                //其他方式，暂不处理
                break;
        }
    }

    //排序规则
    private void queryRule(EsQueryEnum searchEnum, String fieldName, Object value, BoolQueryBuilder boolQuery) {
        switch (searchEnum) {
            //范围查询
            case RANGE_QUERY:
                RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(fieldName);
                String rangeValue = value.toString();
                String[] prices = rangeValue.split("_");
                if (prices.length == 1) {
                    if (rangeValue.startsWith("_")) {
                        rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
                    } else {
                        rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                    }
                } else if (prices.length == 2) {
                    // _6000会截取成["","6000"]
                    if (!prices[0].isEmpty()) {
                        rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
                    }
                    rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
                }
                //filter过滤，不会计算相关性得分
                boolQuery.filter(rangeQueryBuilder);
                break;
            //模糊查询
            case VAGUE_QUERY:
                boolQuery.must(QueryBuilders.matchQuery(fieldName, value).fuzziness(Fuzziness.AUTO));
                break;
            //filter过滤
            case FILTER_QUERY:
                //此处必须进行一次转义，兼容单个value和value集合
                if (value instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<Object> list = (List<Object>) value;
                    boolQuery.filter(QueryBuilders.termsQuery(fieldName, list));
                } else {
                    boolQuery.filter(QueryBuilders.termsQuery(fieldName, value));
                }
                break;
            case MUST_NOT:
                //此处必须进行一次转义，兼容单个value和value集合
                if (value instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<Object> list = (List<Object>) value;
                    boolQuery.mustNot(QueryBuilders.termsQuery(fieldName, list));
                } else {
                    boolQuery.mustNot(QueryBuilders.termsQuery(fieldName, value));
                }
                break;
            default:
                //其他方式，暂不处理
                break;
        }
    }


    /**
     * 创建修改script内容
     *
     * @param data
     * @return
     */
    private <T> StringBuilder buildScriptContext(Object data, Class<T> c) {
        Field[] fields = data.getClass().getFields();
        JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(data));
        List<Field> fieldList = Arrays.asList(c.getFields());
        StringBuilder scriptContext = new StringBuilder();
        String tem = "ctx._source['key']='value'";
        for (Field field : fields) {
            //访问私有属性时必须设置，否则访问不到
            field.setAccessible(true);
            if (field.isAnnotationPresent(EsUpdate.class)) {
                EsUpdate annotation = field.getAnnotation(EsUpdate.class);
                String name = field.getName();
                String fieldName = annotation.name();
                //若注解设置name属性，则使用注解的name属性，否则使用自定义属性
                fieldName = StringUtils.isEmpty(fieldName) ? name : fieldName;
                String finalFieldName = fieldName;
                boolean exist = fieldList.stream().anyMatch(item -> item.getName().equals(finalFieldName));
                if (exist) {
                    scriptContext.append(tem.replace("key", finalFieldName).replace("value", jsonObject.getString(finalFieldName))).append(";");
                }
            }
        }




        return scriptContext;
    }

}
